跳到主要内容

Vue 2:Vue.directive

自定义指令封装直接操作 DOM 的行为。本文使用 Vue 2 钩子;Vue 3 使用 app.directive(),钩子名称改为 mounted、updated、unmounted 等。

注册、钩子与参数​

import Vue from 'vue';

Vue.directive('focus', {
inserted(el) { el.focus(); }
});
const focus = Vue.directive('focus'); // 读取全局指令。

局部注册写在组件 directives 中,模板使用 v-focus。

  • bind:首次绑定,初始化属性。
  • inserted:插入父节点后执行。
  • update:所在组件 VNode 更新时执行,子节点可能还未更新。
  • componentUpdated:所在组件及子节点完成更新后执行。
  • unbind:解绑,清理监听器与手动创建的资源。

钩子接收 el、binding、vnode,更新钩子还接收 oldVnode。binding 包含 name、value、oldValue、expression、arg、modifiers;oldValue 仅在更新钩子中可用。动态参数与对象值可写为 v-pin:[direction]="200"、v-demo="{ color: 'red' }"。

简单指令:颜色、定位与省略​

函数简写会在 bind 和 update 时执行,适合根据值更新样式。

Vue.directive('color', (el, binding) => {
el.style.color = binding.value;
});
Vue.directive('color-swatch', (el, binding) => {
el.style.backgroundColor = binding.value;
});
Vue.directive('pin', (el, binding) => {
el.style.position = 'fixed';
el.style.left = '';
el.style.top = '';
el.style[binding.arg === 'left' ? 'left' : 'top'] = `${binding.value}px`;
});
Vue.directive('ellipsis', (el, binding) => {
Object.assign(el.style, {
width: `${binding.arg || 100}px`,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
});
});
<input v-focus />
<p v-color="'red'">红色文字</p>
<p v-pin:left="200">固定位置</p>
<p v-ellipsis:140>超出宽度时显示省略号</p>

仅控制省略样式时,也可直接使用 CSS。

事件指令的公共清理逻辑​

监听器必须保留函数引用并在解绑时移除。后续事件指令共用以下辅助函数,安装函数返回清理函数。

function withCleanup(install) {
const cleanups = new WeakMap();
return {
inserted(el, binding) {
const cleanup = install(el, binding);
if (cleanup) cleanups.set(el, cleanup);
},
unbind(el) {
cleanups.get(el)?.();
cleanups.delete(el);
}
};
}

该简化版本按初次绑定配置安装;需要动态改变事件类型或目标时,应增加 update 钩子,先清理旧配置再安装。

v-drag:拖动元素​

Vue.directive('drag', withCleanup(el => {
el.style.position = 'relative';
let startX, startY, left, top;
const move = event => {
el.style.left = `${left + event.clientX - startX}px`;
el.style.top = `${top + event.clientY - startY}px`;
};
const up = () => {
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', up);
};
const down = event => {
if (event.button !== 0) return;
event.preventDefault();
startX = event.clientX;
startY = event.clientY;
left = parseFloat(el.style.left) || 0;
top = parseFloat(el.style.top) || 0;
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
};
el.addEventListener('mousedown', down);
return () => {
up();
el.removeEventListener('mousedown', down);
};
}));

鼠标抬起监听放在 document 上,指针移出元素后也能结束拖动。

v-copy:复制文本​

保留单击、双击和附加图标三种模式。下面使用旧项目常见的 execCommand;现代项目可改用 Clipboard API。

function copyText(text) {
const input = document.createElement('textarea');
input.value = text;
input.style.position = 'fixed';
input.style.opacity = '0';
document.body.appendChild(input);
try {
input.select();
return document.execCommand('copy');
} finally {
input.remove();
}
}

Vue.directive('copy', withCleanup((el, binding) => {
const icon = binding.modifiers.icon ? document.createElement('button') : null;
if (icon) {
icon.type = 'button';
icon.textContent = '复制';
el.appendChild(icon);
}
const target = icon || el;
const eventName = binding.modifiers.dblclick ? 'dblclick' : 'click';
const handler = () => {
const clone = el.cloneNode(true);
if (icon) clone.lastChild.remove();
copyText(clone.textContent);
};
target.addEventListener(eventName, handler);
return () => {
target.removeEventListener(eventName, handler);
icon?.remove();
};
}));

使用:v-copy、v-copy.dblclick、v-copy.icon。复制失败时应在业务层提供反馈。

v-screenfull:全屏切换​

此案例依赖 screenfull,需要先安装并导入。

import screenfull from 'screenfull';

Vue.directive('screenfull', withCleanup((el, binding) => {
const icon = binding.modifiers.icon ? document.createElement('span') : null;
if (icon) {
icon.textContent = ' ⛶';
el.appendChild(icon);
}
const toggle = () => {
if (screenfull.isEnabled) screenfull.toggle().catch(console.error);
};
el.addEventListener('click', toggle);
return () => {
el.removeEventListener('click', toggle);
icon?.remove();
};
}));

使用:<button v-screenfull.icon>全屏</button>。

v-backtop:回到顶部​

参数为滚动容器的 id,省略时使用 window;绑定值为显示按钮的滚动阈值。

Vue.directive('backtop', withCleanup((el, binding) => {
const target = binding.arg ? document.getElementById(binding.arg) : window;
if (!target) return;
const threshold = Number(binding.value || 0);
const update = () => {
const top = target === window ? window.scrollY : target.scrollTop;
el.style.visibility = top >= threshold ? 'visible' : 'hidden';
};
const back = () => target.scrollTo({ top: 0, behavior: 'smooth' });
el.addEventListener('click', back);
target.addEventListener('scroll', update, { passive: true });
update();
return () => {
el.removeEventListener('click', back);
target.removeEventListener('scroll', update);
};
}));

使用:<button v-backtop:scroll-box="400">回到顶部</button>。目标必须是实际滚动的容器。

v-click-outside:点击外部关闭​

Vue.directive('click-outside', withCleanup((el, binding) => {
const handler = event => {
if (!el.contains(event.target)) binding.value(event);
};
document.addEventListener('click', handler);
return () => document.removeEventListener('click', handler);
}));

使用:<div v-click-outside="closeCalendar">日历</div>。原案例:DatePicker。

样式指令:扩大点击范围与导航高亮​

v-expand-click​

通过伪元素扩大点击范围,不增加布局占位。上、右、下、左偏移量均为向外扩展的正数。

Vue.directive('expand-click', (el, binding) => {
const [top = 10, right = top, bottom = top, left = right] = binding.value || [];
el.classList.add('expand-click-range');
for (const [name, value] of Object.entries({ top, right, bottom, left })) {
el.style.setProperty(`--expand-${name}`, `${-value}px`);
}
});
.expand-click-range { position: relative; }
.expand-click-range::before {
content: '';
position: absolute;
top: var(--expand-top); right: var(--expand-right);
bottom: var(--expand-bottom); left: var(--expand-left);
}

使用:<button v-expand-click="[20, 30, 40, 50]">点击</button>。范围重叠时需自行处理遮挡。

v-nav-current​

Vue.directive('nav-current', (el, binding) => {
const { curIdx, className, activeClass } = binding.value;
Array.from(el.getElementsByClassName(className)).forEach((item, index) => {
item.classList.toggle(activeClass, index === curIdx);
});
});

使用:v-nav-current="{ curIdx, className: 'nav-item', activeClass: 'active' }"。若导航由 Vue 列表渲染,直接绑定 :class="{ active: index === curIdx }" 更清楚。

Tooltip、空状态与徽标​

这类功能同时涉及内容、样式和交互,优先使用组件,避免在每次指令更新时重复创建 Vue 实例与 DOM。以 Element UI 为例:

<el-tooltip :content="tip"><span>说明</span></el-tooltip>
<div class="content">
<p v-if="empty">{{ emptyText || '暂无数据' }}</p>
<slot v-else />
</div>
<el-badge :value="count" :is-dot="isDot" :type="type"><button>消息</button></el-badge>

Tooltip 接收提示文字与展示属性,空状态接收显示条件、文字和图片,Badge 接收数字、圆点与颜色。若封装为 v-tooltip、v-empty 或 v-badge 指令,额外实例应按元素保存,并在 unbind 中销毁。

参考:Vue 2 自定义指令。